1 Введение

Spooky Author Identification

ЦЕЛЬЮ исследования является попытка предсказать автора по короткому отрывку из написанного им текста методами ML. В данных присутствуют 3 автора: Г.Ф. Лавкрафт (HPL), Э.А. По (EAP) и М.В. Шелли (MWS)

ЗАДАЧИ:

В качестве основных подходов в работе будет рассмотрен XGBoost, нейронные сети(LSTM, FastText, BERT) и наивный байесовский классификатор. Для написания кода будет использоваться язык R.

Несмотря на то, что работа построена вокруг анализа текста, задача классификации авторов не требует специализированной модели, а базируется на качественной подготовке входных признаков. Этапу извлечения признаков и анализу данных в целом будет уделено основное внимание. Также на это решение повлияло сходство этих этапов для широкого круга задач, в том числе и для будущей магистерской работы предобработка и извлечение признаков будут строиться по похожей схеме. Часть из найденных признаков, возможно, далее использоваться не будет и приведена для пополнения инструментария.

Следует также отметить, что признаки для работы с текстом, исходя из вышесказанного, в большинстве задач уже встречались и, поскольку, это первый опыт работы с текстом, они будут частично позамимствованы в интересных решениях, встреченных на просторах Kaggle. Некоторым оправданием такого рода плагиату могут служить, во-первых, возраст (часть написанного кода или не работает, или не особенно оптимально написана), а, во-вторых, язык (подавляющее большинство написано на python).

Работа с моделями будет заключаться в получении предсказаний от достаточно большого числа не самых качественных моделей (порядка 10-ти) и построение поверх них классификатора, принимающего на вход эти предсказания в качестве дополнительных признаков. Предполагается, что это позволит достичь достаточно высоких позиций в соревновании (попадание в топ 20%). В случае, если это предположение не оправдается, будет выбрана одна из моделей (вероятнее всего, XGBoost) для дальнейшей оптимизации. Теи не менее, во многих случаях (на Kaggle), ансамбль средних моделей работает лучше одной оптимизированной.

С исходными данными можно ознакомиться здесь. Данные представлены в виде тренировочного (../input/train.csv) и тестового набора (../input/test.csv). Каждое наблюдение содержит короткий фрагмент текста (Как правило, предложение). Также представлен образец данных в формате, необходимой для загрузки в Kaggle.

2 Подготовка

2.1 Загрузка библиотек

В работе использовались классические библиотеки машинного обучения: keras, caret, xgboost. Для разметки и манипуляций с текстом выбраны tidytext, udpipe, tm, wordcloud. Визуализация проводилась через ggplot, обработка данных средствами tidyverse. Полный список библиотек приведен ниже.

# general visualisation
library('ggplot2') # visualisation
library('scales') # visualisation
library('grid') # visualisation
library('gridExtra') # visualisation
library('RColorBrewer') # visualisation
library('corrplot') # visualisation

# general data manipulation
library('dplyr') # data manipulation
library('readr') # input/output
library('data.table') # data manipulation
library('tibble') # data wrangling
library('tidyr') # data wrangling
library('stringr') # string manipulation
library('forcats') # factor manipulation

# specific visualisation
library('alluvial') # visualisation
library('ggrepel') # visualisation
library('ggridges') # visualisation
library('gganimate') # visualisation
library('ggExtra') # visualisation

# specific data manipulation
library('lazyeval') # data wrangling
library('broom') # data wrangling
library('purrr') # string manipulation
library('reshape2') # data wrangling

# Text / NLP
library('tidytext') # text analysis
library('tm') # text analysis
library('SnowballC') # text analysis
library('topicmodels') # text analysis
library('wordcloud') # test visualisation
library('igraph') # visualisation
library('ggraph') # visualisation
library('babynames') # names

# Models
library('Matrix')
library('xgboost')
library('caret')

library('treemapify') #visualisation

require(tidyverse)
require(tidytext)
require(textstem)
require(qdap)
require(caret)
require(widyr)
require(broom)
require(keras)
require(gridExtra)
require(plotly)
require(scales)
require(ggcorrplot)
# require(RDRPOSTagger)
require(parallel)
require(gmodels)
require(knitr)
require(udpipe)
require(zoo)
require(fastNaiveBayes)
require(e1071)
require(tibbletime)
# require(plotly)
# require(kableExtra)

3 Анализ и визуализация данных

3.1 Структура данных

Набор данных содержит 3 колонки, id, непосредственно текст и метку автора. В тренировочных данных 19579 наблюдений. Встречаются, как короткие, так и длинные отрывки. Приведенный отрывок максимальной длины похож на ошибку в разметке. Точки между предложенриями отсутствуют и данные попали в один отрывок. Возможно, это следует считать выбросом. В тестовых данных 8392 наблюдений и отсутствуют метки классов. Также присутствуют длинные отрывки с отсутствующими точками. Пропушенных данных в наблюдениях не обнаружено.

3.1.1 Тренировочные данные

Первые 5 наблюдений

id text author
id26305 This process, however, afforded me no means of ascertaining the dimensions of my dungeon; as I might make its circuit, and return to the point whence I set out, without being aware of the fact; so perfectly uniform seemed the wall. EAP
id17569 It never once occurred to me that the fumbling might be a mere mistake. HPL
id11008 In his left hand was a gold snuff box, from which, as he capered down the hill, cutting all manner of fantastic steps, he took snuff incessantly with an air of the greatest possible self satisfaction. EAP
id27763 How lovely is spring As we looked from Windsor Terrace on the sixteen fertile counties spread beneath, speckled by happy cottages and wealthier towns, all looked as in former years, heart cheering and fair. MWS
id12958 Finding nothing else, not even gold, the Superintendent abandoned his attempts; but a perplexed look occasionally steals over his countenance as he sits thinking at his desk. HPL
## Observations: 19,579
## Variables: 3
## $ id     <chr> "id26305", "id17569", "id11008", "id27763", "id12958", "i…
## $ text   <chr> "This process, however, afforded me no means of ascertain…
## $ author <chr> "EAP", "HPL", "EAP", "MWS", "HPL", "MWS", "EAP", "EAP", "…
##       id                text              author         
##  Length:19579       Length:19579       Length:19579      
##  Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character

Отрывок максимальной длины

id text author
id27184 Diotima approached the fountain seated herself on a mossy mound near it and her disciples placed themselves on the grass near her Without noticing me who sat close under her she continued her discourse addressing as it happened one or other of her listeners but before I attempt to repeat her words I will describe the chief of these whom she appeared to wish principally to impress One was a woman of about years of age in the full enjoyment of the most exquisite beauty her golden hair floated in ringlets on her shoulders her hazle eyes were shaded by heavy lids and her mouth the lips apart seemed to breathe sensibility But she appeared thoughtful unhappy her cheek was pale she seemed as if accustomed to suffer and as if the lessons she now heard were the only words of wisdom to which she had ever listened The youth beside her had a far different aspect his form was emaciated nearly to a shadow his features were handsome but thin worn his eyes glistened as if animating the visage of decay his forehead was expansive but there was a doubt perplexity in his looks that seemed to say that although he had sought wisdom he had got entangled in some mysterious mazes from which he in vain endeavoured to extricate himself As Diotima spoke his colour went came with quick changes the flexible muscles of his countenance shewed every impression that his mind received he seemed one who in life had studied hard but whose feeble frame sunk beneath the weight of the mere exertion of life the spark of intelligence burned with uncommon strength within him but that of life seemed ever on the eve of fading At present I shall not describe any other of this groupe but with deep attention try to recall in my memory some of the words of Diotima they were words of fire but their path is faintly marked on my recollection It requires a just hand, said she continuing her discourse, to weigh divide the good from evil On the earth they are inextricably entangled and if you would cast away what there appears an evil a multitude of beneficial causes or effects cling to it mock your labour When I was on earth and have walked in a solitary country during the silence of night have beheld the multitude of stars, the soft radiance of the moon reflected on the sea, which was studded by lovely islands When I have felt the soft breeze steal across my cheek as the words of love it has soothed cherished me then my mind seemed almost to quit the body that confined it to the earth with a quick mental sense to mingle with the scene that I hardly saw I felt Then I have exclaimed, oh world how beautiful thou art Oh brightest universe behold thy worshiper spirit of beauty of sympathy which pervades all things, now lifts my soul as with wings, how have you animated the light the breezes Deep inexplicable spirit give me words to express my adoration; my mind is hurried away but with language I cannot tell how I feel thy loveliness Silence or the song of the nightingale the momentary apparition of some bird that flies quietly past all seems animated with thee more than all the deep sky studded with worlds" If the winds roared tore the sea and the dreadful lightnings seemed falling around me still love was mingled with the sacred terror I felt; the majesty of loveliness was deeply impressed on me So also I have felt when I have seen a lovely countenance or heard solemn music or the eloquence of divine wisdom flowing from the lips of one of its worshippers a lovely animal or even the graceful undulations of trees inanimate objects have excited in me the same deep feeling of love beauty; a feeling which while it made me alive eager to seek the cause animator of the scene, yet satisfied me by its very depth as if I had already found the solution to my enquires sic as if in feeling myself a part of the great whole I had found the truth secret of the universe But when retired in my cell I have studied contemplated the various motions and actions in the world the weight of evil has confounded me If I thought of the creation I saw an eternal chain of evil linked one to the other from the great whale who in the sea swallows destroys multitudes the smaller fish that live on him also torment him to madness to the cat whose pleasure it is to torment her prey I saw the whole creation filled with pain each creature seems to exist through the misery of another death havoc is the watchword of the animated world And Man also even in Athens the most civilized spot on the earth what a multitude of mean passions envy, malice a restless desire to depreciate all that was great and good did I see And in the dominions of the great being I saw man reduced? MWS

Отрывок минимальной длины

id text author
id20021 I breathed no longer. EAP

Пропущенные значения

##     id   text author 
##      0      0      0

3.1.2 Тестовые данные

Первые 5 наблюдений

id text
id02310 Still, as I urged our leaving Ireland with such inquietude and impatience, my father thought it best to yield.
id24541 If a fire wanted fanning, it could readily be fanned with a newspaper, and as the government grew weaker, I have no doubt that leather and iron acquired durability in proportion, for, in a very short time, there was not a pair of bellows in all Rotterdam that ever stood in need of a stitch or required the assistance of a hammer.
id00134 And when they had broken down the frail door they found only this: two cleanly picked human skeletons on the earthen floor, and a number of singular beetles crawling in the shadowy corners.
id27757 While I was thinking how I should possibly manage without them, one actually tumbled out of my head, and, rolling down the steep side of the steeple, lodged in the rain gutter which ran along the eaves of the main building.
id04081 I am not sure to what limit his knowledge may extend.
## Observations: 8,392
## Variables: 2
## $ id   <chr> "id02310", "id24541", "id00134", "id27757", "id04081", "id2…
## $ text <chr> "Still, as I urged our leaving Ireland with such inquietude…
##       id                text          
##  Length:8392        Length:8392       
##  Class :character   Class :character  
##  Mode  :character   Mode  :character

Отрывок максимальной длины

id text
id20462 I gasped could not ask that which I longed to know the friendly spirit replied more gravely I have told you that you will not see those whom you mourn But I must away follow me or I must leave you weeping deserted by the spirit that now checks your tears Go I replied I cannot follow I can only sit here grieve long to see those who are gone for ever for to nought but what has relation to them can I listen The spirit left me to groan weep to wish the sun quenched in eternal darkness to accuse the air the waters all all the universe of my utter irremediable misery Fantasia came again and ever when she came tempted me to follow her but as to follow her was to leave for a while the thought of those loved ones whose memories were my all although they were my torment I dared not go Stay with me I cried help me to clothe my bitter thoughts in lovelier colours give me hope although fallacious images of what has been although it never will be again diversion I cannot take cruel fairy do you leave me alas all my joy fades at thy departure but I may not follow thee One day after one of these combats when the spirit had left me I wandered on along the banks of the river to try to disperse the excessive misery that I felt untill overcome by fatigue my eyes weighed down by tears I lay down under the shade of trees fell asleep I slept long and when I awoke I knew not where I was I did not see the river or the distant city but I lay beside a lovely fountain shadowed over by willows surrounded by blooming myrtles at a short distance the air seemed pierced by the spiry pines cypresses and the ground was covered by short moss sweet smelling heath the sky was blue but not dazzling like that of Rome and on every side I saw long allies clusters of trees with intervening lawns gently stealing rivers Where am I? I exclaimed looking around me I beheld Fantasia She smiled as she smiled all the enchanting scene appeared lovelier rainbows played in the fountain the heath flowers at our feet appeared as if just refreshed by dew I have seized you, said she as you slept and will for some little time retain you as my prisoner I will introduce you to some of the inhabitants of these peaceful Gardens It shall not be to any whose exuberant happiness will form an unpleasing contrast with your heavy grief but it shall be to those whose chief care here is to acquired knowledged sic virtue or to those who having just escaped from care pain have not yet recovered full sense of enjoyment This part of these Elysian Gardens is devoted to those who as before in your world wished to become wise virtuous by study action here endeavour after the same ends by contemplation They are still unknowing of their final destination but they have a clear knowledge of what on earth is only supposed by some which is that their happiness now hereafter depends upon their intellectual improvement Nor do they only study the forms of this universe but search deeply in their own minds and love to meet converse on all those high subjects of which the philosophers of Athens loved to treat With deep feelings but with no outward circumstances to excite their passions you will perhaps imagine that their life is uniform dull but these sages are of that disposition fitted to find wisdom in every thing in every lovely colour or form ideas that excite their love Besides many years are consumed before they arrive here When a soul longing for knowledge pining at its narrow conceptions escapes from your earth many spirits wait to receive it and to open its eyes to the mysteries of the universe many centuries are often consumed in these travels and they at last retire here to digest their knowledge to become still wiser by thought and imagination working upon memory When the fitting period is accomplished they leave this garden to inhabit another world fitted for the reception of beings almost infinitely wise but what this world is neither can you conceive or I teach you some of the spirits whom you will see here are yet unknowing in the secrets of nature They are those whom care sorrow have consumed on earth whose hearts although active in virtue have been shut through suffering from knowledge These spend sometime here to recover their equanimity to get a thirst of knowledge from converse with their wiser companions They now securely hope to see again those whom they love know that it is ignorance alone that detains them from them.

Отрывок минимальной длины

id text
id02295 Then he gave a start.

Пропущенные значения

##   id text 
##    0    0

3.2 Пропорции

Тексты По занимают 40%, Шелли 31%, Лавкрафта 29%. Длина предложений у По немного короче, чем у остальных.

## 
##       EAP       HPL       MWS 
## 0.4034935 0.2878084 0.3086981

Медиана выглядит предпочтительнее среднего, вследствие ряда длинных отрывков с пропущеннными точками.

author median text length mean text length
EAP 115 142.2259
HPL 142 155.8435
MWS 130 151.6598

3.3 Облака слов

Облако слов, - удобное представление наиболее часто встречаюшихся слов в тексте. Размер отражает частоту встречаемости. Для начала применим облако к тексту в целом.

Ожидаемо, вспомогательные части речи, - союзы, артикли, местоимения etc в тексте встречаются чаще. Стоит ли от них избавляться при построении модели? Важность признаков будет рассмотрена позже, на этапе feature engineering. Ниже представлен вариант облака без учета вспомогательных слов.

Ключевые слова подтвеждают заявленную тематику текстов, - жизнь и смерть, время и страх. Прослеживаются детективные и даже романтические нотки. Попробуем разделить облака по авторам.

Есть как общие мотивы, например, тематика времени близка всем троим, так и индивидуальные особенности. Тексты Лавкрафта ближе всего к классическому пониманию ужасов. У По можно заметить склонность к детективному стилю. Шелли не чужда романтичность.

3.4 Ключевые слова

Более классическое представление о частоте встречаемости можно построить, используя столбчатые диаграммы. Ниже представлены две диаграммы, первая построена по словам, для второй предварительно использовался стемминг.

Суда по частоте встреч имен Raymond и Perdita у Шелли, можно предположить, что значительная часть ее отрывков взята из одного произведения. Поможет ли это в дальнейшей классификации?

3.5 Уникальные слова

Лексикон Шелли, предоставленный в наборе данных заметно меньше, чем у остальных. Возможно это связано с установленным ранее фактом о больших заимствованиях из одного произведения.

author unique word count
EAP 14856
HPL 14188
MWS 11115

Как видно из графиков, для По и Лавкрафта достаточно примерно 7500 слов, чтобы перекрыть 90% их отрывков. Для Шелли достаточно примерно 5500 слов.

3.7 Корреляция частот совстречаемости

Также стоит посмотреть на результаты корреляционного анализ частот совстречаемости. Еще раз можно убедиться, что у авторов много общего.

3.8 TF-IDF

TF (term frequency — частота слова) — отношение числа вхождений некоторого слова к общему числу слов документа. Таким образом, оценивается важность слова \(t_i\) в пределах отдельного документа.

\[\begin{align} tf(i,d) = \frac{n_{t}}{\sum_k n_{k}} \end{align}\]

где \(n_t\) есть число вхождений слова \(t\) в документ, а в знаменателе — общее число слов в данном документе.

IDF (inverse document frequency — обратная частота документа) — инверсия частоты, с которой некоторое слово встречается в документах коллекции. Учёт IDF уменьшает вес широкоупотребительных слов. Для каждого уникального слова в пределах конкретной коллекции документов существует только одно значение IDF.

\[\begin{align} idf(t,D) = \mbox{log} \frac{|D|}{|\{d_i \in D \ | \ t \in d_i\}|} \end{align}\]

где

  • \(D\) — число документов в коллекции;
  • \({|\{d_i \in D \ | \ t \in d_i\}|}\) — число документов из коллекции \(D\), в которых встречается \(t\) (когда \(n_t \neq 0\)).

Таким образом, мера TF-IDF является произведением двух сомножителей:

\[\begin{align} tf\mbox- idf(t,d,D) = tf(t,d) \times idf(t,D) \end{align}\]

Большой вес в TF-IDF получат слова с высокой частотой в пределах конкретного документа и с низкой частотой употреблений в других документах. Наиболее высокие значения имеют имена собственные. Можно сделать вывод, что Шелли более склонна к их использованию в тексте. Также можно предположить, что эта мера окажется значимой при классификации.

3.8.1 общие униграммы

3.8.2 униграммы по авторам

3.9 Биграммы

Помимо одиночных слов важное значение могут иметь словосочетания, в частности би- и триграммы. Помимо имен собственных здесь начинают появляться характерные сочетания. Как ни странно, и По, и Лавкрафт, оказывается, любили посмеяться. Шелли неравнодушна к сочетаниям, начинающимся на dear. По вспоминает каких-то многочисленных madame, а Лавкрафт нагоняет жути своими lurking fear и ancient house

3.9.1 общие биграммы

3.9.2 биграммы по авторам

3.10 Триграммы

По и Лавкрафт продолжают смеяться, причем По умудряется это делать по-разному, в свободное время подкидывая иностранных словечек. Лавкрафт на все лады вспоминает безумного араба аль Хазреда, а любимый Шелли Реймонд, оказывается, связан со вселенной доктора Кто. Интересные открытия.

3.10.1 общие триграммы

3.10.2 триграммы по авторам

3.11 LDA

Рассматривая задачу сегрегации текста нельзя не остановиться на еще одном популярном подходе, LDA. Латентное размещение Дирихле — применяемая в машинном обучении и информационном поиске порождающая модель, позволяющая объяснять результаты наблюдений с помощью неявных групп, благодаря чему возможно выявление причин сходства некоторых частей данных. Например, если наблюдениями являются слова, собранные в документы, утверждается, что каждый документ представляет собой смесь небольшого количества тем и что появление каждого слова связано с одной из тем документа. LDA является одним из методов тематического моделирования. Тематическое моделирование предназначено для нахождения похожих темы в разных документах и группировки разных слов так, чтобы каждая тема состояла из слов с одинаковым значением.

Число топиков выбирается произвольно. Для начала разумно проверить гипотезу о том, что каждого автора можно описать одним топиком.

##      
##          1    2    3
##   EAP 2608 2495 2736
##   HPL 1802 1975 1832
##   MWS 2115 1932 1972

Авторы практически равномерно распределились по топикам. Очевидно, по трем топикам идентификация автора не имеет смысла. Попробуем увеличить их число до 9.

##      
##          1    2    3    4    5    6    7    8    9
##   EAP  935  886  953  849  699 1019  817  938  743
##   HPL  550  700  603  687  600  611  592  602  664
##   MWS  604  622  527  596  813  530  907  623  797

По сравнению с тремя топиками, особых изменений не обнаруживается. Да, в первом и четвертом топике присутствует Raymond, что должно указывать на Шелли, но остальные авторы в нем тоже присутствуют. К тому же, ключевые слова слишком сильно пересекаются между топиками, например, time присутствует в четырех, а day в пяти. Скорее всего, здесь сыграл роль общий жанр, в котором работали авторы.

4 Feature engineering

4.1 Стилометрические признаки

В качестве основных признаков можно рассматривать следующее:

  • количество слов
  • количество символов
  • среднее количество символов в слове
  • количество слогов
  • среднее количество слогов в слове
  • количество односложных слов
  • количество многосложных слов
  • количество слов, начинающихся с прописной буквы
  • количество слов, в которых все буквы прописные
  • количество уникальных слов
  • количество стоп-слов
  • количество длинных слов (больше n символов)
  • количество пунктуационных символов

Для извлечения слогов используется функция syllable_sum, которая довольно капризно относчится к входным данным. В частности, она выдает ошибки при пропущенных значениях. При обычной конвертации пара строк как раз оказыватся в таком виде (где в исходном тексте были редкие символы, например, ’ΥΠΝΟΣ). Поэтому выполнять придется с ручной очисткой.

Наиболее интересно выглядят признаки, связанные с прописными буквами.

4.2 Признаки, основанные на тональности

4.2.1 Тональность глаголов и интенсификаторы

Эта методика была найдена в одном из примеров и показалась интересной.

thought_verbs <- c('analyze', 'apprehend', 'assume', 'believe', 'calculate', 'cerebrate', 'cogitate',
                   'comprehend', 'conceive', 'concentrate', 'conceptualize', 'conclude', 'consider',
                   'construe', 'contemplate', 'deduce', 'deem', 'delibrate', 'desire', 'diagnose',
                   'doubt', 'envisage', 'envision', 'evaluate', 'excogitate', 'extrapolate', 'fantasize',
                   'forget', 'forgive', 'formulate', 'hate', 'hypothesize', 'imagine', 'infer', 
                   'intellectualize', 'intrigue', 'guess', 'introspect', 'judge', 'know', 'love', 
                   'lucubrate', 'marvel', 'meditate', 'note', 'notice', 'opine', 'perpend', 'philosophize',
                   'ponder', 'question', 'ratiocinate', 'rationalize', 'realize', 'reason', 'recollect', 
                   'reflect', 'remember', 'reminisce', 'retrospect', 'ruminate', 'sense', 'speculate',
                   'stew', 'strategize', 'suppose', 'suspect', 'syllogize', 'theorize', 'think', 
                   'understand', 'visualize', 'want', 'weigh', 'wonder')

loud_verbs <- c('cry', 'exclaim', 'shout', 'roar', 'scream', 'shriek', 'vociferated', 'bawl',
                'call', 'ejaculate', 'retort', 'proclaim', 'announce', 'protest', 'accost', 'declare')

neutral_verbs <- c('say', 'reply', 'observe', 'rejoin', 'ask', 'answer', 'return', 'repeat', 'remark',
                   'enquire', 'respond', 'suggest', 'explain', 'utter', 'mention')

quiet_verbs <- c('whisper', 'murmur', 'sigh', 'grumble', 'mumble', 'mutter', 'whimper', 'hush', 'falter',
                 'stammer', 'tremble', 'gasp', 'shudder')

qualifiers <- c('very', 'too', 'so', 'quite', 'rather', 'little', 'pretty', 'somewhat', 'various', 'almost', 
                'much', 'just', 'indeed', 'still', 'even', 'a lot', 'kind of', 'sort of')

train_tmp <- train %>%
    unnest_tokens(term, text, token = 'ngrams', n=1) %>% 
    bind_rows(train %>% unnest_tokens(term, text, token = 'ngrams', n=2)) %>%
    mutate(term = lemmatize_words(term),
           qualifier_ind = as.integer(term %in% qualifiers),
           thought_verbs_ind = as.integer(term %in% thought_verbs),
           loud_verbs_ind = as.integer(term %in% loud_verbs),
           neutral_verbs_ind = as.integer(term %in% neutral_verbs),
           quiet_verbs_ind = as.integer(term %in% quiet_verbs)) %>%
    group_by(id) %>%
    summarise(qualifier_count = sum(qualifier_ind),
              thought_verbs_count = sum(thought_verbs_ind),
              loud_verbs_count = sum(loud_verbs_ind),
              neutral_verbs_count = sum(neutral_verbs_ind),
              quiet_verbs_count = sum(quiet_verbs_ind))

test_tmp <- test %>%
    unnest_tokens(term, text, token = 'ngrams', n=1) %>% 
    bind_rows(test %>% unnest_tokens(term, text, token = 'ngrams', n=2)) %>%
    mutate(term = lemmatize_words(term),
           qualifier_ind = as.integer(term %in% qualifiers),
           thought_verbs_ind = as.integer(term %in% thought_verbs),
           loud_verbs_ind = as.integer(term %in% loud_verbs),
           neutral_verbs_ind = as.integer(term %in% neutral_verbs),
           quiet_verbs_ind = as.integer(term %in% quiet_verbs)) %>%
    group_by(id) %>%
    summarise(qualifier_count = sum(qualifier_ind),
              thought_verbs_count = sum(thought_verbs_ind),
              loud_verbs_count = sum(loud_verbs_ind),
              neutral_verbs_count = sum(neutral_verbs_ind),
              quiet_verbs_count = sum(quiet_verbs_ind))
 
train_stylometry <- train_stylometry %>% 
    left_join(train_tmp, by = 'id')

test_stylometry <- test_stylometry %>% 
    left_join(test_tmp, by = 'id')

Разделение наблюдается по 4-м признакам из 5.

4.2.2 Sentiment analysis

Для анализа эмоциональной окраски используется tidytext. Это не самый лучший пакет для анализа литературного текста, но он простой и покрывает большую часть nlp задач. В качестве словарей используются AFINN и NRC

Интересным получится показатель afinn, хорошо сегментирующий тексты Лавкрафта, как несущие негативную окраску.

4.3 Признаки, основанные на частях речи (POS-tagging)

В R присутствует большой выбор инструментов для разметки частей речи (а если хочется еще большего, всегда есть reticulate). Здесь используется пакет udpipe. Операция разметки медленная, поэтому отдельными блоками.

4.4 Признаки, основанные на n-граммах

get_author_ngrams <- function(df, author_id){
    df %>% filter(idf == max(idf, na.rm = T) & author == author_id & n > 25) %>% .[,2]
}

df_ngram <- list(author_unigrams_tfidf, author_bigrams_tfidf, author_trigrams_tfidf, author_tetragrams_tfidf,
                author_char_bigrams_tfidf, author_char_trigrams_tfidf, author_char_tetragrams_tfidf,
                author_char_pentagrams_tfidf)

eap <- mapply(get_author_ngrams, df_ngram, rep('EAP', 8)) %>% unlist() %>% unique()
hpl <- mapply(get_author_ngrams, df_ngram, rep('HPL', 8)) %>% unlist() %>% unique()
mws <- mapply(get_author_ngrams, df_ngram, rep('MWS', 8)) %>% unlist() %>% unique()

get_total_ngrams <- function(df){
    map_df(1:4, ~ unnest_tokens(df, term, text, token = 'ngrams', n = .x)) %>%
    bind_rows(map_df(2:5, ~ unnest_tokens(df, term, text, token = 'character_shingles', n = .x,
                                          strip_non_alphanum = F))) %>%
    mutate(EAP_only_ind = as.integer(term %in% eap),
           HPL_only_ind = as.integer(term %in% hpl),
           MWS_only_ind = as.integer(term %in% mws)) %>%
    group_by(id) %>%
    summarise(EAP_only_count = sum(EAP_only_ind),
              HPL_only_count = sum(HPL_only_ind),
              MWS_only_count = sum(MWS_only_ind))
}

train_author_only <- get_total_ngrams(train)
test_author_only <- get_total_ngrams(test)

get_author_pair_ngrams <- function(df, author_id1, author_id2){
    df %>% filter(author == author_id1 | author == author_id2, idf == log(1.5)) %>%
        group_by_at(2) %>% 
        count(wt = n) %>% filter(n > 50) %>% .[,1]
}

eap_hpl <- mapply(get_author_pair_ngrams, df_ngram, rep('EAP', 8), rep('HPL', 8)) %>% unlist() %>% unique()
eap_mws <- mapply(get_author_pair_ngrams, df_ngram, rep('EAP', 8), rep('MWS', 8)) %>% unlist() %>% unique()
hpl_mws <- mapply(get_author_pair_ngrams, df_ngram, rep('HPL', 8), rep('MWS', 8)) %>% unlist() %>% unique()

get_total_pair_ngrams <- function(df){
    map_df(1:4, ~ unnest_tokens(df, term, text, token = 'ngrams', n = .x)) %>%
    bind_rows(map_df(2:5, ~ unnest_tokens(df, term, text, token = 'character_shingles', n = .x,
                                          strip_non_alphanum = F))) %>%
    mutate(EAP_HPL_only_ind = as.integer(term %in% eap_hpl),
           EAP_MWS_only_ind = as.integer(term %in% eap_mws),
           HPL_MWS_only_ind = as.integer(term %in% hpl_mws)) %>%
    group_by(id) %>%
    summarise(EAP_HPL_only_count = sum(EAP_HPL_only_ind),
              EAP_MWS_only_count = sum(EAP_MWS_only_ind),
              HPL_MWS_only_count = sum(HPL_MWS_only_ind))
}

train_author_pair_only <- get_total_pair_ngrams(train)
test_author_pair_only <- get_total_pair_ngrams(test)

Признаки демонстрируют выраженную разделительную способность.

4.5 Гендерные признаки

Проверим, нет ли у авторов предпочтений в области полов.

Лавкрафта, видимо, не особенно интересовали женские пресонажи. Хорошо, что он жил и работал не в наше время. По в общем-то тоже на грани фола. Шелли, ожидаемо, более склонна к женским персонажам. Тем не менее, у всех троих мужской пол доминирует в произведениях.

4.6 Аллитерации и ассонансы

Аллитерация — повторение одинаковых или однородных согласных, ассонанс - повторение гласных. Используется в основном в поэзии для придания особого звучания тексту. Для простоты примем аллитерацию за повторение первых букв в словах, идущих подряд. [a]rab [a]bdul [a]lhazred. Также для большей надежности удалим стоп слова перед проверкой.

get_alliterations <- function(df, stop = F){
    if(stop){
        df <- df %>%
            select(id, text) %>% 
            unnest_tokens(word, text) %>% 
            anti_join(stop_words, by = 'word')
    }else{
        df <- df %>%
            select(id, text) %>% 
            unnest_tokens(word, text)
    }

    df %>% 
        mutate(first = str_sub(word, start = 1, end = 1),
             f_lead_1 = lead(first, n = 1),
             f_lead_2 = lead(first, n = 2),
             id_lead_1 = lead(id, n = 1),
             id_lead_2 = lead(id, n = 2),
             allit2 = first == f_lead_1 & id == id_lead_1,
             allit3 = first == f_lead_1 & first == f_lead_2 & id == id_lead_1 & id == id_lead_2
             ) %>%
        filter(!is.na(allit2)) %>%
        group_by(id, allit2) %>%
        count() %>%
        spread(allit2, n) %>%
        mutate(has_allit = ifelse(!is.na(`TRUE`), 1, 0)) %>%
        select(id, has_allit)
}

train_alliterations <- get_alliterations(train, stop = T)
test_alliterations <- get_alliterations(test, stop = T)

get_allit_plot <- function(df, title){
    df %>% 
        left_join(train, by = 'id') %>% 
        group_by(author, has_allit) %>% 
        count() %>% 
        ungroup() %>% 
        (function(df) left_join(df, df %>% group_by(author) %>% summarise(s = sum(n)), by = 'author')) %>% 
        mutate(n = (n/s) * 100) %>% 
        filter(has_allit == T) %>% 
        ggplot(aes(x = author, y = n, fill = author)) +
        geom_col() +
        scale_fill_manual(values = c(HPL = 'blue4', EAP = 'red4', MWS = 'purple4')) +
        labs(y = "fraction of sentences with alliterations") +
        ggtitle(title) +
        theme_bw()
}

pl1 <- get_allit_plot(get_alliterations(train, stop = T), title = 'without stop words')
pl2 <- get_allit_plot(get_alliterations(train, stop = F), title = 'with stop words')

grid.arrange(pl1, pl2, nrow = 1)

Лавкрафт незначительно опережает остальных в поэтичности речи.

5 Моделирование

5.1 Оценка качества модели

Для оценки качества модели Kaggle предлагает использовать multi-class logarithmic loss. В R эту метрику можно взять из пакета MLmetrics, но следует отметить, что в случае подачи ей на вход датафрейма в некоторых случаях можно получить на выходе NA. Поэтому имеет смысл немного переписать функцию для таких случаев.

\[\begin{align} logloss = -\frac{1}{N}{\sum_{i=1}^N\sum_{j=1}^M y_{ij}log(p_{ij})} \end{align}\]

Здесь \(N\) - число наблюдений, \(M\) - число классов, \(y_{ij}\) = 1, если наблюдение \(i\) принадлежит классу \(j\), иначе 0, \(p_{ij}\) - предсказанные вероятности.

Поскольку работа построена вокруг соревнования на Kaggle, оценка модели на тестовых данных будет производиться непосредственно через саму платформу sunmission. Для тренировочных данных оценка будет производиться непосредственно по получению результата.

5.2 Используемые модели

При построения моделей будут использоваться как найденные выше признаки, так и векторные представления слов. В качестве baseline модели будет использоваться Наивный байесовский классификатор.

Далее будет рассмотрено несколько простых моделей на нейронных сетях с разными входными данными для сбора предсказаний и рекуррентная нейросеть на подготовленных заранее эмбеддингах.

Предсказания предыдущих моделей составят основу для ансамбля, моделировать который будет использован xgboost.

Одной из задач данной работы стоит попадание в топ 20%, что эквивалентно 248 строчке рейтинга или оценке в 0.32237. По достижению этого показателя, основной этап работы будет считаться выполненным.

5.3 Наивный байесовский классификатор

Наивный байесовский классификатор. Это простая модель, активно используемая для начала работы за счет своего быстродействия и, практически, отсутствующих параметров для настройки. В R она реализована во многих пакетах, в частности, e1071, quanteda, klaR. В данной работе используется специализированный пакет fastNaiveBayes, модель из которого работает быстрее остальных на больших объемах данных.

На тренировочных данных оценка MultiLogLoss составляет 0.3091273. Для тестового варианта Kaggle привел оценку в 0.47287. Это далеко не лучший показатель, эквивалентный 764 позиции в рейтинге из 1243. Тем не менее, для первой модели, результат стоящий. Напомним, что пороговый показатель 0.32237.

5.4 Стек

get_ngram_df <- function(in_token, in_n, in_cnt){
    df <- train %>%
        select(-author) %>% 
        unnest_tokens(token, text, token = in_token, n = in_n, to_lower = F) %>% 
        select(-id) %>%
        count(token) %>%
        filter(n > in_cnt) %>%
        .$token
    
    df_cnt <- train %>%
        select(-author) %>%
        bind_rows(test) %>%
        unnest_tokens(token, text, token = in_token, n = in_n, to_lower = F) %>%
        count(id, token) %>%
        filter(token %in% df) %>%
        cast_dtm(id, token, n)
    
    if(in_cnt != 0){
        rowsRemoved <- setdiff(c(train$id,test$id),rownames(df_cnt))
        allZeros <- matrix(0, length(rowsRemoved), ncol(df_cnt), 
                           dimnames = list(rowsRemoved, colnames(df_cnt)))
        df_cnt <- df_cnt %>% rbind(allZeros)
        rm(rowsRemoved,allZeros)
    }
    
    x_train_df <- df_cnt[train$id,] %>% as.matrix
    x_test_df <- df_cnt[test$id,] %>% as.matrix
    rm(df, df_cnt)
        
    return(list(x_train_df, x_test_df))
}

get_ngram_model <- function(df_train, df_test, fold, y_train, filepath){
    nn_model <- keras_model_sequential() %>%
        layer_dense(units = 16, activation = 'relu', input_shape = ncol(df_train)) %>% 
        layer_dense(units = 16, activation = 'relu') %>%
        layer_dense(units = 3, activation = 'softmax')
    
    nn_model %>% compile(
        loss = 'categorical_crossentropy',
        optimizer = 'rmsprop',
        metrics = c('accuracy')
    )
    
    frozen_nn_model <- nn_model %>%
        fit(
            df_train[-fold,], y_train[-fold,],
            batch_size = 2^9,
            epochs = 20,
            validation_split = 0.1, 
            verbose = F,
            callbacks = list(
                callback_early_stopping(monitor = 'val_loss', patience = 2),
                callback_model_checkpoint(
                    filepath = paste0(filepath, '.hdf5'),
                    monitor = 'val_loss',
                    mode = 'min',
                    save_best_only = T)
          )
    )
    nn_model <- load_model_hdf5(paste0(filepath, '.hdf5'))
    
    train_pred <- nn_model %>%
        predict(df_train[fold,])
    test_pred <- nn_model %>%
        predict(df_test)
    fold_eval <- nn_model %>%
        evaluate(df_train[fold,], y_train[fold,])
    out <- list(train_pred = train_pred, test_pred = test_pred, logloss = fold_eval$loss, acc = fold_eval$acc )
    k_clear_session()
    return(out)
}

get_ngram_predictions <- function(ngram_df, filepath){
    train_count <- matrix(0, nrow = nrow(train), ncol = 3)
    test_count <- matrix(0, nrow = nrow(test), ncol = 3)
    metrics_count <- matrix(0, 5, 2)

    for(i in 1:5){
        results_count <- get_ngram_model(ngram_df[[1]], ngram_df[[2]], folds[[i]], y_train, filepath)
        train_count[folds[[i]], ] <- results_count$train_pred
        test_count <- test_count + (results_count$test_pred)/5
        metrics_count[i,1] <- results_count$logloss
        metrics_count[i,2] <- results_count$acc
        gc()
    }   
  
    train_count <- train_count %>%
        as.data.frame()
    test_count <- test_count %>%
        as.data.frame()
    metrics_count <- metrics_count %>%
        as.data.frame()
    rownames(metrics_count) <- paste0("fold ", 1:5, ":")
    return(list(train_count, test_count, metrics_count))
}